/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package and.geo.matrix;

import java.text.FieldPosition;
import java.text.NumberFormat;

import and.awt.geom.AffineTransform;

/**
 * An affine matrix of fixed {@value #SIZE}&times;{@value #SIZE} size. Here, the
 * term "affine" means a matrix with the last row fixed to {@code [0,0,1]}
 * values. Such matrices are used for affine transformations in a 2D space.
 * 
 * <p>
 * This class both extends the <cite>Java2D</cite> {@link AffineTransform} class
 * and implements the {@link Matrix} interface. It allows interoperbility for
 * code that need to pass the same matrix to both <cite>Java2D</cite> API and
 * more generic API working with coordinates of arbitrary dimension.
 * 
 * <p>
 * This class do not implements the {@link XMatrix} interface because the
 * inherited {@code invert()} method (new in J2SE 1.6) declares a checked
 * exception, {@code setZero()} would be an unsupported operation (because it is
 * not possible to change the value at {@code (2,2)}), {@code transpose()} would
 * fails in most cases, and {@code isAffine()} would be useless.
 * 
 * @since 2.3
 * @source $URL$
 * @version $Id$
 * @author Martin Desruisseaux (IRD)
 */
public class AffineTransform2D extends AffineTransform implements Matrix {
	/** Serial number for interoperability with different versions. */
	private static final long serialVersionUID = -9104194268576601386L;

	/** The matrix size, which is {@value} . */
	public static final int SIZE = 3;

	/** Creates a new identity matrix. */
	public AffineTransform2D() {
	}

	/** Constructs a 3&times;3 matrix from the specified affine transform. */
	public AffineTransform2D(final AffineTransform transform) {
		super(transform);
	}

	/**
	 * Creates a new matrix initialized to the same value than the specified
	 * one. The specified matrix size must be {@value #SIZE}&times;
	 * {@value #SIZE}.
	 */
	public AffineTransform2D(final Matrix matrix) {
		if (matrix.getNumRow() != SIZE || matrix.getNumCol() != SIZE) {
			throw new IllegalArgumentException("ILLEGAL_MATRIX_SIZE");
		}
		for (int i = 0; i < SIZE; i++) {
			checkLastRow(i, matrix.getElement(SIZE - 1, i));
		}
		int c = 0;
		final double[] values = new double[6];
		for (int j = 0; j < SIZE - 1; j++) {
			for (int i = 0; i < SIZE; i++) {
				values[c++] = matrix.getElement(j, i);
			}
		}
		assert c == values.length : c;
		// TODO: invokes the super constructor instead if Sun fixes RFE #4093999
		setTransform(values);
	}

	/** Sets this affine transform to the specified flat matrix. */
	private void setTransform(final double[] matrix) {
		setTransform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4],
				matrix[5]);
	}

	/**
	 * Returns the number of rows in this matrix, which is always {@value #SIZE}
	 * in this implementation.
	 */
	public final int getNumRow() {
		return SIZE;
	}

	/**
	 * Returns the number of colmuns in this matrix, which is always
	 * {@value #SIZE} in this implementation.
	 */
	public final int getNumCol() {
		return SIZE;
	}

	/**
	 * Retrieves the value at the specified row and column of this matrix.
	 * 
	 * @param row
	 *            The row number to be retrieved (zero indexed).
	 * @param column
	 *            The column number to be retrieved (zero indexed).
	 * @return The value at the indexed element.
	 */
	public double getElement(final int row, final int column) {
		switch (row) {
		case 0: {
			switch (column) {
			case 0:
				return getScaleX();
			case 1:
				return getShearX();
			case 2:
				return getTranslateX();
			}
			break;
		}
		case 1: {
			switch (column) {
			case 0:
				return getShearY();
			case 1:
				return getScaleY();
			case 2:
				return getTranslateY();
			}
			break;
		}
		case 2: {
			switch (column) {
			case 0: // fall through
			case 1:
				return 0;
			case 2:
				return 1;
			}
			break;
		}
		default: {
			throw new IndexOutOfBoundsException("ILLEGAL_ARGUMENT column");
		}
		}
		throw new IndexOutOfBoundsException(" ILLEGAL_ARGUMENT row");
	}

	/**
	 * Modifies the value at the specified row and column of this matrix.
	 * 
	 * @param row
	 *            The row number to be retrieved (zero indexed).
	 * @param column
	 *            The column number to be retrieved (zero indexed).
	 * @param value
	 *            The new matrix element value.
	 */
	public void setElement(final int row, final int column, final double value) {
		if (row < 0 || row >= SIZE) {
			throw new IndexOutOfBoundsException("ILLEGAL_ARGUMENT row");
		}
		if (column < 0 || column >= SIZE) {
			throw new IndexOutOfBoundsException("ILLEGAL_ARGUMENTcolumn");
		}
		if (row == SIZE - 1) {
			checkLastRow(column, value);
			return; // Nothing to set.
		}
		final double[] matrix = new double[6];
		getMatrix(matrix);
		matrix[row * SIZE + column] = value;
		setTransform(matrix);
		assert Double.compare(getElement(row, column), value) == 0 : value;
	}

	/**
	 * Check if the specified value is valid for the last row if this matrix.
	 * The last row contains only 0 values except the last column which is set
	 * to 1. This method throws an exception if the specified value is not the
	 * expected one.
	 */
	private static void checkLastRow(final int column, final double value)
			throws IllegalArgumentException {
		if (value != (column == SIZE - 1 ? 1 : 0)) {
			throw new IllegalArgumentException("ILLEGAL_ARGUMENT matrix[" + (SIZE - 1) + ','
					+ column + ']');
		}
	}

	/**
	 * Returns a string representation of this matrix. The returned string is
	 * implementation dependent. It is usually provided for debugging purposes
	 * only.
	 */
	@Override
	public String toString() {
		return toString(this);
	}
	 static String toString(final Matrix matrix) {
	        final int numRow = matrix.getNumRow();
	        final int numCol = matrix.getNumCol();
	        StringBuffer buffer = new StringBuffer();
	        final int columnWidth = 12;
	        final String lineSeparator = System.getProperty("line.separator", "\n");
	        final FieldPosition dummy = new FieldPosition(0);
	        final NumberFormat format = NumberFormat.getNumberInstance();
	        format.setGroupingUsed(false);
	        format.setMinimumFractionDigits(6);
	        format.setMaximumFractionDigits(6);
	        for (int j = 0; j < numRow; j++) {
	            for (int i = 0; i < numCol; i++) {
	                final int position = buffer.length();
	                buffer = format.format(matrix.getElement(j, i), buffer, dummy);
	                final int spaces = Math.max(columnWidth - (buffer.length() - position), 1);
	                buffer.insert(position, Utilities.spaces(spaces));
	            }
	            buffer.append(lineSeparator);
	        }
	        return buffer.toString();
	    }

	/** Returns a clone of this affine transform. */
	@Override
	public AffineTransform2D clone() {
		return (AffineTransform2D) super.clone();
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof AffineTransform)) {
			return false;
		}

		AffineTransform a = (AffineTransform) obj;

		return Utilities.equals(getScaleX(), a.getScaleX())
				&& Utilities.equals(getScaleY(), a.getScaleY())
				&& Utilities.equals(getShearX(), a.getShearY())
				&& Utilities.equals(getTranslateX(), a.getTranslateX())
				&& Utilities.equals(getTranslateY(), a.getTranslateY());
	}
}
